package com.cookie.utils;

import com.cookie.config.RedisConfig;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.*;

import java.util.HashSet;
import java.util.Set;

/**
 * 使用 redis pool 连接单机的redis，但不能用于连接redis集群
 * redis集群需要使用 redis cluster
 * 后续可以考虑使用 spring data redis 来连接redis单机和集群
 *
 * jedis cluster是jedis客户端对cluster模式实现的操作类，
 * jedis cluster继承了BinaryJedisCluster类，实现了JedisClusterCommands等接口
 * jedis cluster构造函数调用了父类的构造方法
 * public BinaryJedisCluster(Set<HostAndPort> jedisClusterNode, int connectionTimeout, int soTimeout, int maxAttempts, String user, String password, String clientName, GenericObjectPoolConfig poolConfig, boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters, HostnameVerifier hostnameVerifier, JedisClusterHostAndPortMap hostAndPortMap) {
 *    this.connectionHandler = new JedisSlotBasedConnectionHandler(jedisClusterNode, poolConfig, connectionTimeout, soTimeout, user, password, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier, hostAndPortMap);
 *    this.maxAttempts = maxAttempts;
 * }
 * 而binary jedis cluster的构造方法中，创建了connection handler对象，而
 * JedisSlotBasedConnectionHandler extends JedisClusterConnectionHandler
 * 抽象类 jedis cluster connection handler的构造函数中，创建了jedis cluster info cache对象
 * 并进行了hash slot的初始化，
 * public JedisClusterConnectionHandler(Set<HostAndPort> nodes, GenericObjectPoolConfig poolConfig, int connectionTimeout, int soTimeout, String user, String password, String clientName, boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters, HostnameVerifier hostnameVerifier, JedisClusterHostAndPortMap portMap) {
 *     this.cache = new JedisClusterInfoCache(poolConfig, connectionTimeout, soTimeout, user, password, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier, portMap);
 *     this.initializeSlotsCache(nodes, connectionTimeout, soTimeout, user, password, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
 * }
 * 而jedis cluster info cache中有两个关键属性
 * Map<String, JedisPool> nodes; Map<Integer, JedisPool> slots;
 * initializeSlotsCache方法通过配置的节点信息，循环创建jedis对象，
 * jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort(), connectionTimeout, soTimeout);
 * 并调用jedis cluster info cache对象的this.cache.discoverClusterNodesAndSlots(jedis)方法
 * 只要初始化成功就立即终止循环
 * discover cluster nodes and slots方法中，获取了集群的slot信息
 * List<Object> slots = jedis.clusterSlots();
 * list中每个元素是单独的slot集合(就是后面的slotInfo)，该集合的基本信息为[long,long,list,list]
 * 第一，二个元素为该节点的slot起点和终点，第三个元素为主节点信息，第四个元素为对应的从节点信息，
 * 该list(后面的hostInfos)的基本信息为[string,int,string], 元素分别为host, port, id;
 * 获取有关节点的slot信息后，再调用get assigned slot array方法后去所有的slot值
 * List<Integer> slotNums = this.getAssignedSlotArray(slotInfo);
 * 再获取主节点的地址信息，调用generate host and port
 * HostAndPort targetNode = this.generateHostAndPort(hostInfos);
 * 再根据节点地址来设置节点对应的jedis pool
 * this.setupNodeIfNotExist(targetNode);
 * 如果该节点为主节点，则调用assign slots to node方法，设置每个slot对应的连接池
 * this.assignSlotsToNode(slotNums, targetNode);
 * 至此，jedis cluster对象的初始化完成，通过jedis cluster info cache来保存节点信息及其对应的slot信息
 *
 * redis cluster的批量操作要求所有的key都有相同的slot,以mget为例
 * 该方法中，创建了jedis cluster command匿名对象，调用其run方法来完成操作
 * run方法中，针对所有key, 都通过jedis cluster crc16的get slot方法计算出对应的slot,
 * 再循环判断所有的slot值是否相等，不相等则抛出异常
 * No way to dispatch this command to Redis Cluster because keys have different slots
 * 校验通过则调用run with retries方法，执行具体命令，
 * 该方法可以失败重试，重试次数默认为5，可以通过max attempt配置(大概)
 * 首先通过jedis cluster connection handler的get connection from slot 方法获取该slot对应的jedis对象
 * 在获取slot对应连接时，是通过jedis cluster info cache的get slot pool方法，
 * 若获取的jedis pool为null, 则初始化slot信息，并再尝试获取连接，
 * 如果还为null, 则随机获取一个jedis对象，
 * 获取到jedis后，调用execute方法执行命令，该方法在创建匿名对象时被实现，
 * 当异常为jedis connection exception或jedis redirect exception时，才进行重试，
 * 重试过程中，也会重新初始化slot信息，直到成功执行或重试次数耗尽
 *
 * 官方建议通过key的hash_key来保证slot的一致，实现批量操作
 *
 * 实现cluster模式下跨节点批量操作。
 * 主要通过对key进行区分，然后分别通过pipeline方式获取后，再进行聚合返回
 * 一种方式是通过反射从jedis cluster类中获取connection handler对象属性，
 * 再通过此对象以反射方式获取cache对象属性，通过cache的slots变量获取对应的jedis pool进行分类；
 * 另一种方式是通过jedis cluster对象的get cluster nodes获取集群节点信息，然后通过TreeMap<Long,String>
 * 将主节点负责的slot位置进行存储，通过slot获取节点信息来获取jedis pool;
 * 两种方法本质上都是以key的slot值将key进行分类，主要是在获取jedis pool的方式上有所区别
 */
public class RedisClusterUtil {
    private String addrs;
    private String auth;
    // jedis cluster 对象
    protected JedisCluster cluster;

    public RedisClusterUtil(RedisConfig redisConf){
        addrs = redisConf.getAddrs();
        auth = redisConf.getAuth();

        String[] nodeArray = addrs.split(",");
        // 使用set存放 jedis 的 host and port 作为一个node
        Set<HostAndPort> nodes = new HashSet<>();
        for(String hostAndPort:nodeArray){
            String[] addr = hostAndPort.split(":");
            // System.out.println(addr[0].trim()+"_"+addr[1]);
            nodes.add(new HostAndPort(addr[0].trim(),Integer.parseInt(addr[1].trim())));
        }
        // System.out.println(auth);
        // WRONGPASS invalid username-password pair 密码错误
        // 这里的connection timeout, socket timeout 原本是2000,1000
        cluster = new JedisCluster(nodes,0,0,5,auth, new GenericObjectPoolConfig());

        // cluster.close();
    }
    // 关闭资源，暂时没有用到
    protected void closeResource() {
        if (cluster != null){
            cluster.close();
        }
    }
    // 添加string数据
    public void stringSet(String key, String value) {
        try {
            cluster.set(key, value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

        }
    }
    // 添加hash数据
    public void hashSet(String key, String field, String value) {
        try {

            if (cluster != null) {
                cluster.hset(key, field, value);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

        }
    }

    // 删除key
    public void delKey(String key) {

        try {
            cluster.del(key);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

        }
    }
}
